<?php
 /**
  * LIGA 2.4
  * Autor: Ing. Oscar Galileo García García
  * Creación: 2 de Abril del 2011
  * Última modificación: 8 de Mayo del 2012
  */
 // Función autoload para cargar las clases cuando se solicitan
 function __autoload($clase) {
    require_once $clase.'.php';
 }
 // Funciones para la gestión personalizada de los errores y excepciones
 function excepciones($msj) {
    new Error($msj);
 }
 function errores($errno, $errstr, $errfile, $errline) {
    new Error("Error que dice $errstr ($errno) en el archivo $errfile línea $errline");
 }
 set_exception_handler('excepciones');
 set_error_handler('errores');
 
 // Crea una instancia del objeto LIGA que apunte a una tabla o consulta de la BD
 function LIGA($cad1,$cad2='',$cad3='') {
  return new LIGA($cad1,$cad2,$cad3);
 }
 
 // Núcleo de LIGA, posee todas las funcionalidades básicas del sistema
 class LIGA {
    private $sql1 = '';
    private $sql2 = '';
    private $sql3 = '';
    private $bd = null;
    private $meta = array();
    private $info = array();
    private $bufInsert = array();
    private $bufUpdate = array();
    private $bufDelete = array();
    private $idx = 0;
    // Crea una instancia del objeto LIGA que apunte a una tabla o consulta de la BD
    public function __construct($cad1,$cad2='',$cad3='') {
	if(is_string($cad1)) {
	    $this->bd = require('conector.php');
	    $this->sql1 = $cad1;
	    $this->sql2 = $cad2;
	    $this->sql3 = $cad3;
	} else {
	    throw new Error("Parámetros incorrectos: LIGA($cad1, $cad2, $cad3)");
	}
    }
    // Actualizar y obtener la meta información de la tabla
    private function actMeta() {
	$this->meta = $this->bd->metaInfo($this->sql1);
	return $this->meta;
    }
    // Actualizar y obtener la información de la tabla
    private function actInfo() {
	$this->info = $this->bd->info($this->sql1,$this->sql2,$this->sql3);
	return $this->info;
    }
    // Función que regresa true si alguna de las agujas del arreglo están en el pajar
    private function strpos_array($haystack, $needles) {
        foreach($needles as $str) {
            $pos = strpos($haystack, $str);
            if($pos !== false) {
                return true;
            }
        }
        return false;
    }
    // Obtiene true si la cadena contiene la selección de una tabla o false si es una consulta compleja
    private function tabla_sola($cad) {
        $sqls = array('select ','where ','insert ','delete ','update ','order ','group ','inner ','create ','drop ','alter ','grant ','revoke ','commit ','rollback ');
        $cad = trim($cad);
        if($this->strpos_array($cad,$sqls)) {
            return false;
            //return array(0,$cad);
        } elseif(strpos($cad,'.')!==false) {
            return true;
        } else {
            return true;
        }
    }
    // Cambia y obtiene un dato en la $info mediante el índice, la columna y el valor nuevo
    public function cambiar($idx,$col,$valor) {
        return $this->info['info'][$idx][$col] = $valor;
    }
    // Obtener la meta información almacenada o la carga de la BD
    public function meta() {
	if(count($this->meta)>0) {
	    return $this->meta;
	} else {
	    return $this->actMeta();
	}
    }
    // Obtener los registros almacenados o las carga de la BD
    public function info() {
	if(count($this->info)>0) {
	    return $this->info;
	} else {
	    return $this->actInfo();
	}
    }
    // Agrega al buffer un registro para insertar en la tabla sólo si pasa la validación, sino obtiene los mensajes de error
    public function insertar($datos,$act=false) {
        if(is_array($datos) && count($datos)>0 && $this->tabla_sola($this->sql1)) {
            $tablas = array();
            $bien = true;
            $tabla;
            foreach($datos as $col => $val) {
	        // Ignora las columnas que no existen en el objeto
                if($this->col_existe($col)) {
                    $tabla = $this->col_prop($col,'tabla');
                    $msj = '';
                    // Si no acepta valores nulos rechazar cadenas vacías
		    if($val=='' && $this->col_prop($col,'pri') && !$this->col_prop($col,'ai')) {
			$msj = "El campo $col no puede quedar vacío (llave primaria).";
                        $bien = false;
		    }
		    // Rechazar valores vacíos para columnas que no pueden ser nulas
		    if($val=='' && !$this->col_prop($col,'nulo')) {
		        if($msj=='') {
			    $msj = "El campo $col no puede quedar vacío.";
			} else {
			    $msj .= " El campo $col no puede quedar vacío.";
			}
                        $bien = false;
		    }
		    // Ignorar valores vacíos ya
		    if($val==='' && $msj==='') {
		        continue;
		    }
                    // Recorta la cadena al máximo permitido...
		    $max = $this->col_prop($col,'max');
		    if($max>0 && $max < strlen($val)) {
		        $val = substr($val,0,$max);
		    }
		    // Rechazar el valor si no es numérico y el tipo del campo sí
                    if($this->col_numerica($col) && !is_numeric($val)) {
                        if($msj=='') {
			    $msj = "El campo $col sólo admite valores numéricos.";
			} else {
			    $msj .= " El campo $col sólo admite valores numéricos.";
			}
                        $bien = false;
                    }
                    // Validamos fechas y horas opcionales
		    if(($this->col_prop($col,'type')==='timestamp' || $this->col_prop($col,'type')==='datetime' || $this->col_prop($col,'type')==='date') && !ereg('^[0-9]{4}([-/:._][0-9]{2}){2}( ([0-9]{2}[-/:._]){2}[0-9]{2})?',$val)) {
		        if($msj=='') {
			    $msj = "El campo $col sólo admite fecha y hora en formato estándar: YYYY-MM-DD HH:mm:ss.";
			} else {
			    $msj .= " El campo $col sólo admite fecha y hora en formato estándar: YYYY-MM-DD HH:mm:ss.";
			}
			$bien = false;
		    }
		    // Validamos horas
		    if($this->col_prop($col,'type')==='time' && !ereg('^([0-5][0-9][-/:._]?){2}([0-9]{2})?',$val)) {
		        if($msj=='') {
			    $msj = "El campo $col sólo admite la hora en formato estándar: HH:mm:ss.";
			} else {
			    $msj .= " El campo $col sólo admite la hora en formato estándar: HH:mm:ss.";
			}
			$bien = false;
		    }
                    if($msj=='') {
                        $tablas[$tabla][$col] = array('dato'=>$val,'codif'=>$this->col_prop($col,'codif'),'num'=>$this->col_prop($col,'num'),'msj'=>'');
                    } else {
                        $tablas['errores'][$col] = array('dato'=>$val,'msj'=>$msj);
                    }
                }
            }
            if($bien) {
                $this->bufInsert[] = $tablas;
            } else {
                return $tablas;
            }
        } else {
            throw new Error('Debe seleccionar sólo una tabla.');
        }
	// Actualizar después de una sola ejecución
	if($act)
	 return $this->actualizar(true);
	else
	 return true;
    }
    // Modifica registros existentes mediante el valor de la llave primaria o condición where
    public function modificar($datos,$act=false) {
        $llaves = $this->col_llave();
        if(count($datos)>0 && count($llaves)==1) {
            foreach($datos as $llave => $dats) {
                if(count($dats)>0) {
                    foreach($dats as $col => $val) {
                        if(!$this->col_existe($col) || $val=='') {
                            unset($dats[$col]);
                        }
                    }
                    $this->bufUpdate[$llave] = $dats;
                }
            }
        } else {
            throw new Error('Debe hacer referencia a una sola tabla sin llaves primarias compuestas');
        }
	// Actualizar después de una sola ejecución
	if($act)
	 return $this->actualizar(true);
	else
	 return true;
    }
    // Elimina uno o varios registros de la tabla a través de la llave primaria o condición where
    public function eliminar($datos,$act=false) {
        // Sólo acumula en el buffer las llaves primarias a eliminar
        $llaves = $this->col_llave();
        if(is_numeric($datos) && count($llaves)==1) {
            $this->bufDelete[] = $datos;
        } elseif(is_string($datos) && count($llaves)==1) {
            $datos = explode(',',$datos);
            foreach($datos as $dat) {
                $this->bufDelete[] = $dat;
            }
        } elseif(is_array($datos) && count($llaves)==1) {
            foreach($datos as $dat) {
                $this->bufDelete[] = $dat;
            }
        } else {
            throw new Error('Debe seleccionar sólo una tabla.');
        }
	// Actualizar después de una sola ejecución
	if($act)
	 return $this->actualizar(true);
	else
	 return true;
    }
    // Ejecuta las consultas pendientes, regresa un arreglo con el número de registros insertados, modificados y eliminados o los errores de SQL
    private function ejecutar() {
        $ret = array('insertados'=>0,'modificados'=>0,'eliminados'=>0);
	if(count($this->bufInsert)>0) {
	    // Enviar consulta de inserciones
	    $resp = $this->bd->insertar($this->bufInsert);
	    $this->bufInsert = array();
	    $ret['insertados'] = $resp;
	}
	if(count($this->bufUpdate)>0) {
	    // Enviar consulta de actualizaciones
	    $resp = $this->bd->actualizar($this->bufUpdate,$this->sql1,$this->col_llave());
	    $this->bufUpdate = array();
            $ret['modificados'] = $resp;
	}
	if(count($this->bufDelete)>0) {
	    // Enviar consulta de eliminaciones
	    $resp = $this->bd->eliminar($this->bufDelete,$this->sql1,$this->col_llave());
	    $this->bufDelete = array();
	    $ret['eliminados'] = $resp;
	}
        return $ret;
    }
    // Saber si una columna existe en el objeto
    public function col_existe($col) {
	$col = trim($col);
        return (is_string($col) && array_key_exists($col,$this->meta()));
    }
    // Saber si una columna contiene valores numéricos
    public function col_numerica($col) {
        return ($this->col_existe($col) && $this->meta[$col]['num']);
    }
    // Obtener el valor de cualquier propiedad de una columna si existe
    public function col_prop($col,$prop) {
	$col = trim($col);
	$prop = trim($prop);
        if($this->col_existe($col) && is_string($prop) && $prop!='') {
            if(array_key_exists($prop,$this->meta[$col])) {
                return $this->meta[$col][$prop];
            }
        }
        return false;
    }
    // Obtener un arreglo con las llaves primarias encontradas
    public function col_llave() {
	$llaves = array();
	$meta = $this->meta();
	foreach($meta as $col => $dato) {
	    if($this->col_prop($col,'pri')) {
		$llaves[$this->col_prop($col,'tabla')][] = $col;
	    }
	}
	if(count($llaves)>0) {
	    return $llaves;
	} else {
	    return false;
	}
    }
    // Obtiene el número de registros en el objeto
    public function numReg() {
	$info = $this->info();
	return $info['numReg'];
    }
    // Obtiene el número de columnas en el objeto
    public function numCol() {
	$info = $this->info();
	return $info['numCol'];
    }
    // Ejecuta las operaciones pendientes y obtiene el número de filas afectadas en cada caso (si se pasa true forzamos a actualizar)
    public function actualizar($forzar=false) {
        $resp = $this->ejecutar();
	if($resp['insertados']>0 || $resp['modificados']>0 || $resp['eliminados']>0 || $forzar) {
            $this->info = array();
	}
	$this->idx = 0;
        return $resp;
    }
    // Obtiene un dato de las filas mediante el índice del registro y el nombre de la columna
    public function dato($idx,$col) {
	$col = trim($col);
        if($this->col_existe($col)) {
	    $info = $this->info();
	    return isset($info['info'][$idx][$col]) ? $info['info'][$idx][$col] : false;
	} else {
            return "LIGA: Columna no encontrada ($col)";
	}
    }
    // Obtiene una fila (arreglo asociativo) mediante el índice del registro
    public function fila($idx) {
	$info = $this->info();
	return isset($info['info'][$idx]) ? $info['info'][$idx] : false;
    }
    // Obtiene cada fila (arreglo asociativo) para iterar sobre los elementos del objeto
    public function filas() {
	$info = $this->info();
	if(isset($info['info'][$this->idx])) {
	    return $info['info'][$this->idx++];
	} else {
	    $this->idx = 0;
	    return false;
	}
    }
    // Obtiene los registros que coinciden con los parámetros de búsqueda
    public function buscar($q) {
        $r = array();
	if(count($q)===0) return $r;
	while($f = $this->filas()) {
	    $i = (array_intersect($q,$f));
	    if(count($i)>0)
	     $r[] = $f;
	}
	return $r;
    }
    // Obtiene los valores de una columna como arreglo
    public function columna($col) {
        if($this->col_existe($col)) {
            $info = $this->info();
            $datos = array();
            foreach($info['info'] as $fila) {
                $datos[] = $fila[$col];
            }
            return $datos;
        } else {
            throw new Error("La columna seleccionada no apareció en la tabla ($col)");
        }
    }
    // Intercambia los nombres de las variables por sus valores o propiedades según el registro de la tabla indicado y la columna
    public function vars($idx,$cad,$comillas = false) {
        if(is_integer($idx) && is_string($cad) && $cad!='' && (strpos($cad,'@col[')!==false || strpos($cad,'@tab[')!==false)) {
	    // Los nombres de las columnas
            if(strpos($cad,'@col[')!==false) {
                // Contiene el nombre de una columna, si es válido lo intercambia, sino pone un error ahí...
                $ini = strpos($cad,'@col[')+5;
                $fin = strpos($cad,']',$ini);
                $pars = substr($cad,$ini,$fin-$ini);
		// Aquí hay la alternativa de usar parámetros extras
		$parss = explode(',',$pars,2);
		$col = $parss[0];
		// Agregar la opción de seleccionar columnas con enteros @col[1]
		if(is_numeric(trim($col)) && ($ent = intval(trim($col)))) {
		    $llaves = array_keys($this->meta());
		    if(count($llaves)>=$ent) {
		        for($i=0;$i<count($llaves);$i++) {
			    if($i===($ent-1)) {
			        $col = $llaves[$i];
				break;
			    }
			}
		    }
		}
		$prop = '';
		if(isset($parss[1])) {
		    $prop = $parss[1];
		}
		// Para obtener el valor de las propiedades existentes, sino una cadena vacía
		if($prop!='') {
		    $reg = $this->col_prop($col,$prop);
		    if($comillas) {
			$reg = "'$reg'";
		    }
		    if($reg) {
			$cad = str_replace("@col[$pars]",$reg,$cad);
		    } else {
			$cad = str_replace("@col[$pars]","",$cad);
		    }
		} else {
		    $reg = str_replace('@','&#64;',htmlentities($this->dato($idx,$col),ENT_QUOTES,'UTF-8'));
		    if($comillas) {
			$reg = "'$reg'";
		    }
		    $cad = str_replace("@col[$pars]",$reg,$cad);
		}
            }
	    if(strpos($cad,'@tab[')!==false) {
		// Contiene una variable con alguna propiedad de la tabla
		$ini = strpos($cad,'@tab[')+5;
                $fin = strpos($cad,']',$ini);
		$pars = substr($cad,$ini,$fin-$ini);
		// Aquí hay la alternativa de usar parámetros extras
		$parss = explode(',',$pars,1);
		if($prop = $parss[0]) {
		    $prop = trim($prop);
		    $num = 0;
		    if($prop=='numReg') {
			$num = $this->numReg();
		    } elseif($prop=='numCol') {
			$num = $this->numCol();
		    }
		    if($comillas) {
			$num = "'$num'";
		    }
		    $cad = str_replace("@tab[$pars]",$num,$cad);
		}
	    }
            $cad = $this->vars($idx,$cad,$comillas);
        }
        return $cad;
    }
    // Ejecuta con eval lo que está dentro de llaves @{...}@ y lo incrusta en el texto
    public function ejec($cad) {
	if(is_string($cad) && $cad!='' && strpos($cad,'@{')!==false && strpos($cad,'}@')!==false) {
	    $ini = strpos($cad,'@{')+2;
            $fin = strpos($cad,'}@',$ini);
	    $orig = substr($cad,$ini,$fin-$ini);
	    $ejec = trim($orig);
            if($ejec!='') {
                try {
                    if(strpos($ejec,'if')===false) {
                        $resultado = eval("return ($ejec);");
                    } else {
                        $resultado = eval($ejec);
                    }
                    $cad = str_replace('@{'.$orig.'}@',$resultado,$cad);
                }catch(Error $e) {
                    $cad = str_replace('@{'.$orig.'}@',"Error al intentar evaluar ( $orig )",$cad);
                }
            } else {
                $cad = str_replace('@{'.$orig.'}@','',$cad);
            }
	    $cad = $this->ejec($cad);
	}
	return $cad;
    }
    // Evalúa la expresión condicional para el registro indicado y obtiene true o false según sea el caso
    public function cond($idx,$cad) {
	if(is_integer($idx) && is_string($cad) && $cad!='') {
	    $cad = $this->vars($idx,$cad,true); # Con comillas para las comparaciones
	    if(strpos($cad,'@{')===false) {
		return $this->ejec('@{'.$cad.'}@');
	    } else {
		return $this->ejec($cad);
	    }
	}
	return false;
    }
    // Imprime en pantalla todos los registros de acuerdo a una cadena dada con variables y ejecuciones
    public function mostrarRegistros($cad,$ini=false,$max=false) {
	if(is_string($cad) && $cad!='' && strpos($cad,'@')!==false) {
            // Cambio de variables
            if($max===false) {
                $max = $ini;
                $ini = false;
            }
	    $info = $this->info();
            // Contador para mostrar sólo el máximo de registros seleccionados
            $cont = 0;
            $contar = ($max) ? true : false;
            # Se acaba lo del máximo
	    if($info['numReg']>0 && $info['numCol']>0) {
		foreach($info['info'] as $idx => $campos) {
                    // Para comenzar a partir de $ini si fue declarado
                    if($ini && $idx<$ini) {
                        continue;
                    }
		    $registro = $this->vars($idx,$cad);
		    echo $this->ejec($registro);
                    // Terminará en el máximo número de registro seleccionado
                    if($contar) {
                        $cont += 1;
                        if($max==$cont) {
                            return $max;
                        }
                    } # Se acaba lo del máximo
		}
		return $info['numReg'];
	    } else {
                echo 'No se encontraron registros en '.$this->sql1.' '.$this->sql2;
                return false;
	    }
	} else {
	    throw new Error("No especificó una cadena válida para ser procesada ($cad)");
	}
    }
    // Obtiene los elementos del objeto LIGA en el formato descrito manualmente usando las variables especiales
    public function elementos($cad,$ini=false,$max=false) {
        if($this->numCol()==0) {
            throw new Error('El objeto LIGA proporcionado no tiene columnas válidas');
        }
        if($this->numReg()==0) {
            return '<div>No se encontraron registros</div>';
        }
        if(!is_string($cad) || $cad=='') {
            throw new Error("El segundo parámetro debe ser una cadena válida ($cad)");
        }
        ob_start();
        $this->mostrarRegistros($cad,$ini,$max);
        return ob_get_clean();
    }
 }
?>